#!/usr/bin/env python
# coding=utf-8

import curses
import sys
import subprocess
from optparse import OptionParser
from datetime import datetime

class Lerp:
	"""Linear intERPolation
	
	v = command value
	c = screen column
	"""
	def __init__(self, v0, v1, c0, c1):
		# c = f(v) = a * v + b
		self.a = (c1 - c0) / (v1 - v0)
		self.b = c0 - self.a * v0
	
	def vtocol(self, v):
		return int(round(self.a * v + self.b))

	def coltov(self, c):
		return (c - self.b) / self.a

class TPlot:
	def __init__(self, options, args):
		self.options = options
		self.cmd = args
		self.cmd_str = ' '.join(self.cmd)
		if self.options.shell:
			self.cmd = self.cmd_str
		self.data = []
	
	def run_cmd(self):
		p = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=self.options.shell)
		out, err = p.communicate()
		status = p.wait()
		if status != 0:
			raise Exception(err)
		
		try:
			return float(out)
		except Exception:
			raise Exception("Expected a single number from command; got: " + out)

	def draw_title(self):
		width = self.screen.getmaxyx()[1]
		title = ("tplot: " + self.cmd_str)[:width]
		self.screen.addstr(title.center(width), curses.A_STANDOUT)

	def getlimit(self, useroption, default):
		if useroption is None:
			return default
		return float(useroption)

	def draw_axis(self):
		self.screen.addstr('|', curses.A_BOLD)

	def draw(self):
		self.screen.clear()
		self.draw_title()
			
		maxy, maxx = self.screen.getmaxyx()
		values = self.data[-(maxy - 2):]
		values_str = ['%G' % v for v in values]

		maxvalue = self.getlimit(self.options.max, max(0, max(values)))
		minvalue = self.getlimit(self.options.min, min(0, min(values)))

		colmin = max(len(s) for s in values_str) + 1
		colmax = maxx - 1

		lerp = Lerp(minvalue, maxvalue, colmin, colmax)
		colzero = lerp.vtocol(0)

		for i, v in enumerate(values):
			self.screen.addstr(values_str[i].rjust(colmin - 1) + ' ')
			col = lerp.vtocol(v)
			_from = max(min(col, colzero), colmin)
			_to = min(max(col, colzero), colmax)
			spaces_before = ' ' * (_from - colmin)
			bar = '=' * max(0, _to - _from)
			self.screen.addstr(spaces_before)
			if v >= 0:
				self.draw_axis()
				bar = bar[1:]
			self.screen.addstr(bar)
			if v < 0: self.draw_axis()
			self.screen.addstr('\n')
		
		self.screen.refresh()

	def initscreen(self, screen):
		self.screen = screen
		for i in range(1, curses.COLORS):
			curses.init_pair(i, i, curses.COLOR_BLACK)
	
	def loop(self, screen):
		self.initscreen(screen)
		while True:
			self.data.append(self.run_cmd())
			if len(self.data) > self.screen.getmaxyx()[0] - 2:
				self.data = self.data[1:]

			delay = self.options.delay * 1000
			while delay > 0:
				self.draw()
				
				screen.timeout(delay)
				t0 = datetime.now()
				c = self.screen.getch()

				if c == -1: break
				if c == ord('q') or c == ord('Q'):
					return
				delay -= int ((datetime.now() - t0).microseconds / 1000)

strbool = ("disabled", "enabled")

def main():
	parser = OptionParser(
		usage="%prog [options] command ...",
		version="%prog 0.3",
		description="Execute a program periodically, plotting the output over time"
	)
	defaults = {
		"delay": 1,
		"shell": False,
	}
	parser.set_defaults(**defaults)
	parser.disable_interspersed_args()
	parser.add_option(
		"-d", "--delay",
		type="int", dest="delay",
		help="delay in seconds between calls to command (default: %(delay)d)" % defaults
	)
	parser.add_option(
		"-s", "--shell",
		action="store_true", dest="shell",
		help="execute command through system shell (default: %s)" % (strbool[defaults["shell"]],)
	)
	parser.add_option(
		"-m", "--min-value",
		type="int", dest="min",
		help="minimum axis value (default: auto)"
	)
	parser.add_option(
		"-M", "--max-value",
		type="int", dest="max",
		help="maximum axis value (default: auto)"
	)
	(options, args) = parser.parse_args()
	
	if options.delay <= 0:
		parser.error("delay must be positive.")

	if len(args) < 1:
		parser.error("Missing command to be executed.")
	
	curses.wrapper(TPlot(options, args).loop)

if __name__ == '__main__':
	main()


